Skip to content

feat: 添加无缝播放,与 auto mix 合并,统一切歌过渡模式#957

Open
WorldSansha wants to merge 9 commits intoimsyy:devfrom
WorldSansha:new-gapless-playback
Open

feat: 添加无缝播放,与 auto mix 合并,统一切歌过渡模式#957
WorldSansha wants to merge 9 commits intoimsyy:devfrom
WorldSansha:new-gapless-playback

Conversation

@WorldSansha
Copy link
Contributor

  • 新增 songTransitionMode 三选一设置(关闭/Auto Mix/Gapless),替代独立的 enableAutomix 开关
  • 通过 Pinia getter 向后兼容 enableAutomix / useGaplessPlayback,现有 automix 代码无需修改
  • 新增 AudioBufferPlayer + GaplessManager 实现无缝播放(预解码 AudioBuffer + 采样级精确调度)
  • 提取共享 getNextSongInfo() 到 PlayerController,automix 和 gapless 复用下一曲确定逻辑
  • 统一 refreshNextPreload() 入口:始终执行 URL 预取,gapless 额外触发 buffer 预解码
  • 播放列表变更检测复用 automix 的懒校验模式(onTimeUpdate 中比对),不在变更方法中加判断
  • 修复 BaseAudioPlayer 渐入音量闪烁和 stop() 冻结共享 AudioContext
  • 修复 pause 事件在歌曲自然结束时取消 gapless 调度的时序问题
  • 添加设置迁移 v12:enableAutomix → songTransitionMode

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在通过引入无缝播放功能和统一歌曲过渡模式来显著提升音频播放体验。它将现有的自动混音功能与新的无缝播放机制整合到一个单一的设置中,简化了用户配置。核心改动包括新的音频播放器和管理器,以及对现有播放逻辑的重构和优化,以确保平滑、无缝的歌曲切换,并修复了几个与音频处理相关的播放问题。

Highlights

  • 统一歌曲过渡模式: 引入了一个新的 songTransitionMode 设置,取代了原有的 enableAutomix 开关,允许用户在“关闭”、“自动混音 (Auto Mix)”和“无缝播放 (Gapless)”之间选择歌曲切换方式。此设置在播放设置界面中以选择框形式呈现,并根据所选模式动态显示相关子设置。
  • 新增无缝播放功能: 实现了基于 AudioBufferPlayerGaplessManager 的无缝播放功能。AudioBufferPlayer 负责播放预解码的音频缓冲区,实现采样级精确调度;GaplessManager 则管理下一首歌曲的预加载、解码、调度和提交,确保歌曲之间平滑过渡。
  • 播放逻辑共享与优化: 将获取下一首歌曲信息的逻辑 getNextSongInfo() 提取到 PlayerController 中,供自动混音和无缝播放功能复用。同时,统一了 refreshNextPreload() 入口,确保在所有模式下都能进行 URL 预取,并在无缝播放模式下额外触发 AudioBuffer 预解码。
  • 修复音频播放器问题: 修复了 BaseAudioPlayer 中渐入音量闪烁的问题,通过直接操作 gainNode 避免了 cancelScheduledValues 的冲突。此外,修复了 stop() 方法可能导致共享 AudioContext 冻结的问题,以及 pause 事件在歌曲自然结束时取消无缝调度的时间序问题。
  • 设置迁移与兼容性: 更新了设置架构版本至 12,并添加了迁移逻辑,将旧的 enableAutomix 布尔值设置自动转换为新的 songTransitionMode 枚举值。同时,通过 Pinia getter 提供了 enableAutomixuseGaplessPlayback 的向后兼容性,确保现有代码无需修改。
Changelog
  • src/components/Setting/config/play.ts
    • 为“预载下一首”设置添加了 forceIf 条件,当无缝播放启用时强制开启预载
    • 将“启用自动混音”开关替换为“切歌过渡模式”选择框,提供“关闭”、“自动混音”和“无缝播放”选项
    • 更新了自动混音的警告对话框,并为无缝播放添加了新的警告对话框
    • 根据所选的 songTransitionMode 动态显示“最大分析时间”设置
  • src/core/audio-player/BaseAudioPlayer.ts
    • 修改了渐入播放逻辑,直接操作 gainNode 以避免 cancelScheduledValues 导致的音量闪烁
    • 更新了 stop() 方法,在暂停时使用 keepContextRunning 选项,防止共享 AudioContext 冻结
  • src/core/automix/AutomixManager.ts
    • 重构了 getNextSongForAutomix 方法,现在调用 usePlayerController().getNextSongInfo() 来获取下一首歌曲信息
  • src/core/gapless/AudioBufferPlayer.ts
    • 新增了 AudioBufferPlayer 类,用于播放预解码的 AudioBuffer,支持采样级精确调度
  • src/core/gapless/GaplessManager.ts
    • 新增了 GaplessManager 类,负责管理无缝播放的预加载、解码、调度、提交和清除生命周期
  • src/core/player/AudioManager.ts
    • 引入了 AudioBufferPlayeruseGaplessManager
    • 添加了 _replayGain 缓存,用于引擎切换时同步增益值
    • 修改了 destroy() 方法,以清除 GaplessManager 的状态
    • 更新了 play() 方法,处理 AudioBufferPlayer 引擎的切换,并在 URL 不匹配时清除 GaplessManager 预载
    • 新增了 restoreDefaultEngine() 方法,用于从 AudioBufferPlayer 切换回默认播放引擎
    • 新增了 commitGaplessTransition() 方法,用于接管 GaplessManager 中预调度的 AudioBufferPlayer 作为当前播放引擎
    • 更新了 setReplayGain() 方法,以缓存当前的增益值
  • src/core/player/PlayerController.ts
    • 引入了 useGaplessManager
    • 修改了 reset() 方法,以清除 GaplessManager 的状态
    • 移除了对 songManager.prefetchNextSong() 的直接调用,并移除了 songManager 的导入
    • 新增了 refreshNextPreload() 方法,统一了 URL 预取和无缝播放的 AudioBuffer 预解码逻辑
    • 新增了 handleGaplessSwitch() 方法,用于在无缝过渡完成后同步 UI 和播放状态
    • 更新了 pause 事件监听器,在非无缝过渡调度时取消 GaplessManager 的调度
    • 更新了 seeking 事件监听器,以取消 GaplessManager 的调度
    • 修改了 ended 事件监听器,优先尝试无缝过渡,失败则回退到标准切歌逻辑
    • timeupdate 事件中添加了无缝播放的懒校验和调度逻辑,复用自动混音的监控模式
    • 修改了 play() 方法中的条件判断,以兼容 AudioBufferPlayer 的空 src 属性
    • getNextSongInfo() 方法从 AutomixManager 移动到 PlayerController,实现共享逻辑
  • src/stores/migrations/settingMigrations.ts
    • CURRENT_SETTING_SCHEMA_VERSION 更新为 12
    • 添加了版本 12 的迁移函数,将旧的 enableAutomix 布尔值转换为新的 songTransitionMode 枚举值
  • src/stores/setting.ts
    • enableAutomix 属性替换为 songTransitionMode,类型为联合类型 "off" | "automix" | "gapless"
    • 更新了 songTransitionMode 的默认值为 "off"
    • 新增了 enableAutomixuseGaplessPlayback 的 Pinia getter,以提供向后兼容性
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR introduces gapless playback functionality and unifies song transition modes. The review identified a critical security vulnerability in GaplessManager related to unvalidated URL handling, which could lead to SSRF or DoS. Additionally, several code quality and maintainability improvements were suggested for AudioBufferPlayer and GaplessManager, including addressing duplicate logic, redundant code, and encapsulation issues. All original comments have been retained as they align with best practices and are not contradicted by the provided rules.

Comment on lines +77 to +88
const response = await fetch(url, {
signal: abortController.signal,
});

// 检查是否已取消
if (abortController.signal.aborted) return;

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const arrayBuffer = await response.arrayBuffer();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The preload method in GaplessManager accepts an unvalidated url parameter which is directly passed to the fetch API. In an Electron environment, this can lead to Server-Side Request Forgery (SSRF) or unauthorized local file access if the attacker can control the song URL (e.g., via a malicious playlist). Furthermore, since the entire response is loaded into memory using response.arrayBuffer(), an attacker could provide a URL to a very large file to cause a Denial of Service (DoS) by exhausting the renderer process's memory.

Recommendation:

  1. Validate the url parameter to ensure it uses allowed protocols (e.g., http:, https:) and points to trusted domains.
  2. Check the Content-Length header of the response before calling response.arrayBuffer() to ensure the file size is within reasonable limits.

Comment on lines +10 to +276
export class AudioBufferPlayer extends BaseAudioPlayer {
/** 预解码的音频缓冲区 */
private buffer: AudioBuffer | null = null;
/** 当前活动的 SourceNode */
private sourceNode: AudioBufferSourceNode | null = null;
/** 是否处于暂停状态 */
private _paused = true;
/** 播放速率 */
private _rate = 1.0;

/** 锚点偏移量(秒) */
private anchorOffset = 0;
/** 锚点时刻的 AudioContext 时间 */
private anchorContextTime = 0;

/** timeupdate 定时器 */
private timeupdateTimer: ReturnType<typeof setInterval> | null = null;

/** 引擎能力描述 */
public override readonly capabilities: EngineCapabilities = {
supportsRate: true,
supportsSinkId: false,
supportsEqualizer: true,
supportsSpectrum: true,
};

constructor() {
super();
}

/**
* 注入预解码的 AudioBuffer
*/
public setBuffer(buffer: AudioBuffer) {
this.buffer = buffer;
}

// 音频图谱初始化回调(无需创建 MediaElement)
protected onGraphInitialized(): void {
// 空实现
}

// AudioBufferPlayer 不支持 URL 加载
public async load(_url: string): Promise<void> {
// 空实现,buffer 通过 setBuffer 注入
}

/**
* 创建并启动 SourceNode
*/
protected async doPlay(): Promise<void> {
if (!this.buffer || !this.audioCtx || !this.inputNode) return;

// 清理旧的 source
this.stopSource();

const source = this.audioCtx.createBufferSource();
source.buffer = this.buffer;
source.playbackRate.value = this._rate;
source.connect(this.inputNode);

// 记录锚点
this.anchorContextTime = this.audioCtx.currentTime;
source.start(0, this.anchorOffset);

source.onended = () => {
if (this.sourceNode === source && !this._paused) {
// 检查是否真正播放完毕
const elapsed = this.currentTime;
const dur = this.duration;
if (dur > 0 && elapsed < dur - 0.5) {
console.warn(
`[AudioBufferPlayer] source.onended 提前触发 (elapsed=${elapsed.toFixed(2)}, duration=${dur.toFixed(2)})`,
);
return;
}
this._paused = true;
this.stopTimeupdateTimer();
this.dispatch(AUDIO_EVENTS.ENDED);
}
};

this.sourceNode = source;
this._paused = false;
this.startTimeupdateTimer();
this.dispatch(AUDIO_EVENTS.PLAY);
}

/**
* 精确调度播放(无缝衔接时使用)
* @param offset 音频偏移量(秒)
* @param when AudioContext 时间点
*/
public scheduleStart(offset: number, when: number) {
if (!this.buffer || !this.audioCtx || !this.inputNode) return;

this.stopSource();

const source = this.audioCtx.createBufferSource();
source.buffer = this.buffer;
source.playbackRate.value = this._rate;
source.connect(this.inputNode);

this.anchorOffset = offset;
this.anchorContextTime = when;
source.start(when, offset);

source.onended = () => {
if (this.sourceNode === source && !this._paused) {
const elapsed = this.currentTime;
const dur = this.duration;
if (dur > 0 && elapsed < dur - 0.5) {
console.warn(
`[AudioBufferPlayer] scheduled source.onended 提前触发 (elapsed=${elapsed.toFixed(2)}, duration=${dur.toFixed(2)})`,
);
return;
}
this._paused = true;
this.stopTimeupdateTimer();
this.dispatch(AUDIO_EVENTS.ENDED);
}
};

this.sourceNode = source;
this._paused = false;
this.startTimeupdateTimer();
}

protected doPause(): void {
if (this._paused) return;
// 记录当前位置
this.anchorOffset = this.currentTime;
this.stopSource();
this._paused = true;
this.stopTimeupdateTimer();
this.dispatch(AUDIO_EVENTS.PAUSE);
}

protected doSeek(time: number): void {
this.anchorOffset = Math.max(0, Math.min(time, this.duration));
if (this.audioCtx) {
this.anchorContextTime = this.audioCtx.currentTime;
}

// 如果正在播放,重新创建 source
if (!this._paused) {
this.stopSource();

if (this.buffer && this.audioCtx && this.inputNode) {
const source = this.audioCtx.createBufferSource();
source.buffer = this.buffer;
source.playbackRate.value = this._rate;
source.connect(this.inputNode);
source.start(0, this.anchorOffset);

source.onended = () => {
if (this.sourceNode === source && !this._paused) {
const elapsed = this.currentTime;
const dur = this.duration;
if (dur > 0 && elapsed < dur - 0.5) return;
this._paused = true;
this.stopTimeupdateTimer();
this.dispatch(AUDIO_EVENTS.ENDED);
}
};

this.sourceNode = source;
this.anchorContextTime = this.audioCtx.currentTime;
}
}

this.dispatch(AUDIO_EVENTS.SEEKED);
}

public setRate(value: number): void {
const old = this._rate;
this._rate = value;

// 更新锚点以保持位置准确
if (this.audioCtx && !this._paused) {
this.anchorOffset = this.currentTime;
this.anchorContextTime = this.audioCtx.currentTime;
}

if (this.sourceNode) {
this.sourceNode.playbackRate.value = value;
}

// 速率变化后需要重新计算锚点
if (old !== value && this.audioCtx && !this._paused) {
this.anchorContextTime = this.audioCtx.currentTime;
}
}

public getRate(): number {
return this._rate;
}

protected async doSetSinkId(_deviceId: string): Promise<void> {
// AudioBufferPlayer 不支持独立设备切换,依赖共享 AudioContext
}

public get src(): string {
return "";
}

public get duration(): number {
return this.buffer?.duration ?? 0;
}

public get currentTime(): number {
if (this._paused || !this.audioCtx) return this.anchorOffset;
const wallDelta = this.audioCtx.currentTime - this.anchorContextTime;
return Math.max(0, Math.min(this.anchorOffset + wallDelta * this._rate, this.duration));
}

public get paused(): boolean {
return this._paused;
}

public getErrorCode(): number {
return 0;
}

/**
* 销毁引擎,释放内存
*/
public override destroy(): void {
this.stopSource();
this.stopTimeupdateTimer();
this.buffer = null;
this._paused = true;
super.destroy();
}

/** 停止当前 SourceNode */
private stopSource() {
if (this.sourceNode) {
try {
this.sourceNode.onended = null;
this.sourceNode.stop();
this.sourceNode.disconnect();
} catch {
// 可能已经停止
}
this.sourceNode = null;
}
}

/** 启动 timeupdate 定时器 */
private startTimeupdateTimer() {
this.stopTimeupdateTimer();
this.timeupdateTimer = setInterval(() => {
if (!this._paused) {
this.dispatch(AUDIO_EVENTS.TIME_UPDATE);
}
}, 200);
}

/** 停止 timeupdate 定时器 */
private stopTimeupdateTimer() {
if (this.timeupdateTimer) {
clearInterval(this.timeupdateTimer);
this.timeupdateTimer = null;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

AudioBufferPlayer 的实现中有一些可以改进的地方,以提高代码的可维护性和清晰度:

  1. 重复的 onended 逻辑:在 doPlayscheduleStartdoSeek 方法中,创建 AudioBufferSourceNode 和设置其 onended 回调的逻辑存在重复。此外,doSeek 方法中的 onended 回调缺少了对事件提前触发的警告日志。建议将这部分逻辑提取到一个私有辅助方法中。
  2. setRate 方法中冗余代码:在 setRate 方法的末尾,anchorContextTime 被重复赋值。if (old !== value && this.audioCtx && !this._paused) 这个判断块可以移除,因为 anchorContextTime 在前面已经被更新。

通过重构可以使代码更简洁且易于维护。

Comment on lines +138 to +142
// 直接操作 gainNode.gain 设置音量,不调用 setVolume 避免污染 volume 字段
if (this.player["gainNode"]) {
const gainNode = this.player["gainNode"] as GainNode;
gainNode.gain.setValueAtTime(volume, when);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

schedule 方法中,通过 this.player["gainNode"] 的方式访问了 AudioBufferPlayer 的受保护(protected)成员 gainNode。这种方式破坏了类的封装性。

建议在 BaseAudioPlayerAudioBufferPlayer 中添加一个公共方法来处理音量的定时调度,例如 scheduleVolumeAtTime(volume, when)。这样可以使 GaplessManager 的实现更清晰,并保持 AudioBufferPlayer 的封装。

例如,在 BaseAudioPlayer 中添加:

public scheduleVolumeAtTime(value: number, when: number) {
    if (this.gainNode && this.audioCtx) {
        // 考虑 replayGain
        const targetValue = value * this.replayGain;
        this.gainNode.gain.setValueAtTime(targetValue, when);
    }
}

然后在 GaplessManager 中调用 this.player.scheduleVolumeAtTime(volume, when)

@imsyy imsyy marked this pull request as draft March 1, 2026 16:00
WorldSansha and others added 2 commits March 4, 2026 13:43
- 新增 songTransitionMode 三选一设置(关闭/Auto Mix/Gapless),替代独立的 enableAutomix 开关
- 通过 Pinia getter 向后兼容 enableAutomix / useGaplessPlayback,现有 automix 代码无需修改
- 新增 AudioBufferPlayer + GaplessManager 实现无缝播放(预解码 AudioBuffer + 采样级精确调度)
- 提取共享 getNextSongInfo() 到 PlayerController,automix 和 gapless 复用下一曲确定逻辑
- 统一 refreshNextPreload() 入口:始终执行 URL 预取,gapless 额外触发 buffer 预解码
- 播放列表变更检测复用 automix 的懒校验模式(onTimeUpdate 中比对),不在变更方法中加判断
- 修复 BaseAudioPlayer 渐入音量闪烁和 stop() 冻结共享 AudioContext
- 修复 pause 事件在歌曲自然结束时取消 gapless 调度的时序问题
- 添加设置迁移 v12:enableAutomix → songTransitionMode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
prefetchNextSong() 用 playIndex+1 不处理 DJ 跳过,导致与
getNextSongInfo() 返回不同歌曲,ID 校验失败使 gapless 预载被跳过。
改为直接用 getAudioSource(nextInfo.song) 获取正确的 URL。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@WorldSansha WorldSansha force-pushed the new-gapless-playback branch from 7723910 to 262bcac Compare March 4, 2026 05:45
WorldSansha and others added 2 commits March 4, 2026 14:04
- 将 doPlay、scheduleStart、doSeek 中重复的 SourceNode 创建与 onended
  回调提取为 createAndStartSource 辅助方法
- 修复 doSeek 的 onended 回调缺失提前触发警告日志的问题
- 修复 setRate 中 anchorOffset 使用新速率计算的错误,改为先用旧速率
  计算当前位置再更新 _rate
- 移除 setRate 末尾冗余的 anchorContextTime 赋值

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
forceIf 仅在 UI 层强制显示,不会修改 store 中的实际值,
导致关闭"下一首预载"后无缝播放的 AudioBuffer 预解码被完全跳过

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@WorldSansha WorldSansha marked this pull request as ready for review March 4, 2026 06:14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@WorldSansha WorldSansha marked this pull request as draft March 4, 2026 10:03
WorldSansha and others added 2 commits March 5, 2026 16:28
- nextOrPrev 循环跳过 DJ 歌后在 playSong 中无法再次命中检测,
  改为在循环中收集被跳过的歌名并统一弹出提示
- 右键"下一首播放"添加 DJ 歌时提前警告用户该歌曲将被跳过

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GaplessManager 增加 songId 字段和 updateNextIndex 方法
- 懒校验用 songId 判断歌曲是否变化,索引变化但歌曲相同时
  只更新索引不重新预载(节省带宽和解码时间)
- handleGaplessSwitch 取不到歌时回退到标准切歌

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@WorldSansha
Copy link
Contributor Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 实现了无缝播放功能,并将其与自动混音(Auto Mix)功能合并,统一为“切歌过渡模式”设置。这是一个重要的功能增强,代码实现质量很高。主要变更包括:新增 songTransitionMode 设置项;通过 AudioBufferPlayerGaplessManager 实现无缝播放;重构 PlayerController 以统一预加载逻辑;修复 BaseAudioPlayer 中的一些问题;并添加了必要的设置迁移。代码结构清晰,对边界情况和向后兼容性考虑周全。我只发现一处可以优化代码重复的地方,具体见文件评论。

Comment on lines +458 to 482
if (v === "automix") {
window.$dialog.warning({
title: "启用自动混音 (Beta)",
content:
"可能出现兼容性问题,该功能在早期测试,遇到问题请反馈issue,不保证可以及时处理。效果可能因为歌曲而异,保守策略。",
positiveText: "开启",
negativeText: "取消",
onPositiveClick: () => {
settingStore.enableAutomix = true;
settingStore.songTransitionMode = "automix";
},
});
} else if (v === "gapless") {
window.$dialog.warning({
title: "启用无缝播放 (Beta)",
content:
"无缝播放会预解码下一首歌曲的音频数据,每首歌曲约占用 50-150MB 内存。如果设备内存较小,可能影响性能。该功能目前处于预览状态,有任何问题请提交 Issues。",
positiveText: "开启",
negativeText: "取消",
onPositiveClick: () => {
settingStore.songTransitionMode = "gapless";
},
});
} else {
settingStore.enableAutomix = v;
settingStore.songTransitionMode = v;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了减少代码重复并提高可维护性,可以对 automixgapless 的处理逻辑进行合并。这两个分支的结构几乎完全相同,只是标题和内容不同。

                if (v === "automix" || v === "gapless") {
                  const titles = {
                    automix: "启用自动混音 (Beta)",
                    gapless: "启用无缝播放 (Beta)",
                  };
                  const contents = {
                    automix:
                      "可能出现兼容性问题,该功能在早期测试,遇到问题请反馈issue,不保证可以及时处理。效果可能因为歌曲而异,保守策略。",
                    gapless:
                      "无缝播放会预解码下一首歌曲的音频数据,每首歌曲约占用 50-150MB 内存。如果设备内存较小,可能影响性能。该功能目前处于预览状态,有任何问题请提交 Issues。",
                  };
                  window.$dialog.warning({
                    title: titles[v],
                    content: contents[v],
                    positiveText: "开启",
                    negativeText: "取消",
                    onPositiveClick: () => {
                      settingStore.songTransitionMode = v;
                    },
                  });
                } else {
                  settingStore.songTransitionMode = v;
                }

@WorldSansha WorldSansha marked this pull request as ready for review March 5, 2026 08:47
WorldSansha and others added 2 commits March 5, 2026 16:47
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setupSongUI 未重置 statusStore.duration,切歌时滑块 max 残留上一首时长,
用户拖到超出新歌时长的位置后 seek 到末尾触发 ended 导致跳过。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant